﻿using System.Windows;
using System.Diagnostics;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Red.Controls.Diagrams
{
    /// <summary>
    /// Defines an arrow that has multiple points.
    /// </summary>
    public class CurvedArrow : Shape
    {
        #region Dependency property definitions

        public static readonly DependencyProperty ArrowHeadLengthProperty =
            DependencyProperty.Register( "ArrowHeadLength", typeof( double ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( 20.0, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty ArrowHeadWidthProperty =
            DependencyProperty.Register( "ArrowHeadWidth", typeof( double ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( 12.0, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty ArrowVisibleProperty =
            DependencyProperty.Register( "ArrowVisible", typeof( bool ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( true, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty PointsProperty =
            DependencyProperty.Register( "Points", typeof( PointCollection ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.Register( "IsActive", typeof( bool ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( false, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty IsValidProperty =
            DependencyProperty.Register( "IsValid", typeof( bool ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( false, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty IsStraightProperty =
            DependencyProperty.Register( "IsStraight", typeof( bool ), typeof( CurvedArrow ),
                new FrameworkPropertyMetadata( false, FrameworkPropertyMetadataOptions.AffectsRender ) );

        #endregion Dependency property definitions

        #region Properties

        /// <summary>
        /// The angle (in degrees) of the arrow head.
        /// </summary>
        public double ArrowHeadLength
        {
            get { return ( double )GetValue( ArrowHeadLengthProperty ); }
            set { SetValue( ArrowHeadLengthProperty, value ); }
        }

        /// <summary>
        /// The width of the arrow head.
        /// </summary>
        public double ArrowHeadWidth
        {
            get { return ( double )GetValue( ArrowHeadWidthProperty ); }
            set { SetValue( ArrowHeadWidthProperty, value ); }
        }

        /// <summary>
        /// Visibility of the arrow head.
        /// </summary>
        public bool ArrowVisible
        {
            get { return ( bool )GetValue( ArrowVisibleProperty ); }
            set { SetValue( ArrowVisibleProperty, value ); }
        }

        /// <summary>
        /// The intermediate points that make up the line between the start and the end.
        /// </summary>
        public PointCollection Points
        {
            get { return ( PointCollection )GetValue( PointsProperty ); }
            set { SetValue( PointsProperty, value ); }
        }

        /// <summary>
        /// Specifies if connection is active or inactive (with cross in the middle of the line).
        /// </summary>
        public bool IsActive
        {
            get { return ( bool )GetValue( IsActiveProperty ); }
            set { SetValue( IsActiveProperty, value ); }
        }

        /// <summary>
        /// Specifies if connection is valid.
        /// </summary>
        public bool IsValid
        {
            get { return ( bool )GetValue( IsValidProperty ); }
            set { SetValue( IsValidProperty, value ); }
        }

        /// <summary>
        /// Specifies if connection is presented as straight line.
        /// </summary>
        public bool IsStraight
        {
            get { return ( bool )GetValue( IsStraightProperty ); }
            set { SetValue( IsStraightProperty, value ); }
        }

        #endregion Properties

        #region Private methods

        /// <summary>
        /// Return the shape's geometry.
        /// </summary>
        protected override Geometry DefiningGeometry
        {
            get
            {
                if ( Points == null || Points.Count < 2 )
                {
                    var geometryGroup = new GeometryGroup();
                    geometryGroup.Freeze();
                    return geometryGroup;
                }

                //
                // Geometry has not yet been generated.
                // Generate geometry and cache it.
                //
                var geometry = GenerateGeometry();

                var group = new GeometryGroup();
                group.Children.Add( geometry );

                if ( ArrowVisible )
                    GenerateArrowHeadGeometry( group );

                //
                // Return cached geometry.
                //
                group.Freeze();
                return group;
            }
        }

        /// <summary>
        /// Generate the geometry for the three optional arrow symbols at the start, middle and end of the arrow.
        /// </summary>
        private void GenerateArrowHeadGeometry( GeometryGroup geometryGroup )
        {
            var penultimatePoint = Points[ Points.Count - 2 ];
            var arrowHeadTip = Points[ Points.Count - 1 ];
            var startDir = arrowHeadTip - penultimatePoint;
            startDir.Normalize();
            var basePoint = arrowHeadTip - ( startDir * ArrowHeadLength );
            var crossDir = new Vector( -startDir.Y, startDir.X );

            var arrowHeadPoints = new Point[ 3 ];
            arrowHeadPoints[ 0 ] = arrowHeadTip;
            arrowHeadPoints[ 1 ] = basePoint - ( crossDir * ( ArrowHeadWidth / 2 ) );
            arrowHeadPoints[ 2 ] = basePoint + ( crossDir * ( ArrowHeadWidth / 2 ) );

            //
            // Build geometry for the arrow head.
            //
            var arrowHeadFig = new PathFigure { IsClosed = true, IsFilled = true, StartPoint = arrowHeadPoints[ 0 ] };
            var lineSegment1 = new LineSegment( arrowHeadPoints[ 1 ], true );
            lineSegment1.Freeze();
            var lineSegment2 = new LineSegment( arrowHeadPoints[ 2 ], true );
            lineSegment2.Freeze();
            arrowHeadFig.Segments.Add( lineSegment1 );
            arrowHeadFig.Segments.Add( lineSegment2 );
            arrowHeadFig.Freeze();

            var pathGeometry = new PathGeometry();
            pathGeometry.Figures.Add( arrowHeadFig );
            pathGeometry.Freeze();

            geometryGroup.Children.Add( pathGeometry );
            geometryGroup.Freeze();
        }

        #endregion Private methods

        #region Protected methods

        /// <summary>
        /// Generate the shapes geometry.
        /// </summary>
        protected Geometry GenerateGeometry()
        {
            var pathGeometry = new PathGeometry();

            if ( IsStraight )
            {
                // Make a straight line from first and last point from Points collection
                var fig = new PathFigure { IsClosed = false, IsFilled = false, StartPoint = Points[ 0 ] };

                var lineSegment = new LineSegment( Points[ Points.Count - 1 ], true );
                lineSegment.Freeze();
                fig.Segments.Add( lineSegment );

                fig.Freeze();
                pathGeometry.Figures.Add( fig );
            }
            else if ( Points.Count == 2 || Points.Count == 3 )
            {
                // Make a straight line.
                var fig = new PathFigure { IsClosed = false, IsFilled = false, StartPoint = Points[ 0 ] };

                for ( var i = 1; i < Points.Count; ++i )
                {
                    var lineSegment = new LineSegment( Points[ i ], true );
                    lineSegment.Freeze();
                    fig.Segments.Add( lineSegment );
                }
                fig.Freeze();
                pathGeometry.Figures.Add( fig );
            }
            else
            {
                var adjustedPoints = new PointCollection { Points[ 0 ] };
                for ( var i = 1; i < Points.Count; ++i )
                {
                    adjustedPoints.Add( Points[ i ] );
                }
                adjustedPoints.Freeze();

                if ( adjustedPoints.Count == 4 )
                {
                    // Make a curved line.
                    var fig = new PathFigure { IsClosed = false, IsFilled = false, StartPoint = adjustedPoints[ 0 ] };
                    var bezierSegment = new BezierSegment( adjustedPoints[ 1 ], adjustedPoints[ 2 ], adjustedPoints[ 3 ], true );
                    bezierSegment.Freeze();
                    fig.Segments.Add( bezierSegment );

                    fig.Freeze();
                    pathGeometry.Figures.Add( fig );
                }
                else if ( adjustedPoints.Count >= 5 )
                {
                    // Make a curved line.
                    var fig = new PathFigure { IsClosed = false, IsFilled = false, StartPoint = adjustedPoints[ 0 ] };

                    adjustedPoints.RemoveAt( 0 );

                    while ( adjustedPoints.Count > 3 )
                    {
                        var generatedPoint = adjustedPoints[ 1 ] + ( ( adjustedPoints[ 2 ] - adjustedPoints[ 1 ] ) / 2 );

                        var bezierSegment = new BezierSegment( adjustedPoints[ 0 ], adjustedPoints[ 1 ], generatedPoint, true );
                        bezierSegment.Freeze();
                        fig.Segments.Add( bezierSegment );

                        adjustedPoints.RemoveAt( 0 );
                        adjustedPoints.RemoveAt( 0 );
                    }

                    if ( adjustedPoints.Count == 2 )
                    {
                        var bezierSegment = new BezierSegment( adjustedPoints[ 0 ], adjustedPoints[ 0 ], adjustedPoints[ 1 ], true );
                        bezierSegment.Freeze();
                        fig.Segments.Add( bezierSegment );
                    }
                    else
                    {
                        Trace.Assert( adjustedPoints.Count == 2 );

                        var bezierSegment = new BezierSegment( adjustedPoints[ 0 ], adjustedPoints[ 1 ], adjustedPoints[ 2 ], true );
                        bezierSegment.Freeze();
                        fig.Segments.Add( bezierSegment );
                    }

                    fig.Freeze();
                    pathGeometry.Figures.Add( fig );
                }
            }

            pathGeometry.Freeze();
            return pathGeometry;
        }

        #endregion Protected methods
    }
}
